﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

namespace OvercomingShyness
{
    class BadKeyCompiler
    {
        int mGenSym;
        List<string> included;
        List<string> requirements;

        public BadKeyCompiler(string filename, BadKeyMachine owner)
        {
            // included keeps from loading the same library twice
            included = new List<string>();

            // add the base file first
            included.Add(filename.ToLower());

            // also keep redundency out of requirements
            requirements = new List<string>();
        }

        /// <summary>
        /// Function to display compiler diagnostics or not.
        /// </summary>
        /// <param name="text"></param>
        public void Debug(string text)
        {
            // uncomment if you want to see dumps of compilation stages
            System.Diagnostics.Debug.WriteLine(text);
        }

        public string[] Compile(string[] input)
        {
            // mGenSym can be used to generate unique variable names
            // be sure to increment it, i'm leaving it up to you. D:
            mGenSym = 0;

            // read the script and process it a bunch
            input = PreprocessUsing(input);
            input = PreprocessRequirements(input);

            Debug("\n-------------------------\n"
            + "Agregated References\n-------------------------\n");
            Debug(string.Join("\n", input.ToArray()));

            input = PreprocessLineEscapes(input);
            input = PreprocessNormalize(input);

            Debug("\nValidating syntax...\n");
            ValidateSyntax(input);
            Debug("Syntax valid.\n");

            input = ProcessFunctionEquals(input);
            input = ProcessEquals(input);
            input = ProcessElse(input);
            input = ProcessIf(input);
            input = ProcessFor(input);
            input = ProcessWhile(input);

            Debug("\n-------------------------\n"
            + "Finished Compiling\n-------------------------\n");

            return input;
        }

        /// <summary>
        /// Preprocessor to fold all libraries into one blob
        /// </summary>
        /// <param name="input">base script</param>
        /// <returns>a blob</returns>
        public string[] PreprocessUsing(string[] input)
        {
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                if (input[i].Trim().StartsWith("using"))
                {
                    string filename = input[i].Substring(6).ToLower();

                    // recursively traverse any files that have not already been visited
                    if (!included.Contains(filename))
                    {
                        included.Add(filename);
                        StreamReader s = new StreamReader(filename);
                        string[] foldIn = PreprocessUsing(s.ReadToEnd().Replace("\r", "").Split('\n'));
                        foreach (string line in foldIn)
                        {
                            acc.Add(line);
                        }
                    }
                }
                else
                {
                    acc.Add(input[i]);
                }
            }

            return acc.ToArray();
        }

        /// <summary>
        /// accumulate all requirements from all included files and prune redundency
        /// </summary>
        /// <param name="input">script to parse</param>
        /// <returns>script with validate_requirements function added in</returns>
        public string[] PreprocessRequirements(string[] input)
        {
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                // gather all lines starting with 'requiring'
                if (input[i].Trim().StartsWith("requiring"))
                {
                    string requirement = input[i].Substring(10).ToLower();
                    if (!requirements.Contains(requirement))
                    {
                        requirements.Add(requirement);
                    }
                }
                else
                {
                    acc.Add(input[i]);
                }
            }

            // generate the function validate_requirements
            List<string> header = new List<string>();
            header.Add("###########");
            header.Add("def validate_requirements ()");
            foreach (string line in requirements)
            {
                header.Add("requiring " + line);
            }
            header.Add("return");
            header.Add("###########");

            return header.Concat(acc).ToArray();
        }

        /// <summary>
        /// Removes line endings that are escaped by dashes
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public string[] PreprocessLineEscapes(string[] input)
        {
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                string line = input[i];

                if (line.Trim().EndsWith("-"))
                {
                    List<string> chain = new List<string>();
                    // .*? is for non-greedy
                    chain.Add(Regex.Replace(input[i], @"^\s*-?(.*?)-?\s*$", "$1"));

                    while (input[i].EndsWith("-"))
                    {
                        i++;
                        if (!input[i].Trim().StartsWith("-"))
                        {
                            throw new Exception("Expected '-' at beginning of line: " + input[i]);
                        }
                        chain.Add(Regex.Replace(input[i], @"^\s*-?(.*?)-?\s*$", "$1"));
                    }

                    acc.Add(String.Join("", chain.ToArray()));
                }
                else
                {
                    acc.Add(line);
                }
            }

            Debug("\n-------------------------\n"
            + "Preprocessing Line Escapes\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));

            return acc.ToArray();
        }

        /// <summary>
        /// Preprocessor to normalize white-space
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>normalized script</returns>
        public string[] PreprocessNormalize(string[] input)
        {
            List<string> acc = new List<string>();
            List<string> comments = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                // grab a line
                string line = input[i];

                // and keep the strings in it somewhere safe from gsub
                string[] strings;
                line = RemoveStrings(line, out strings);

                // trim all leading and trailing whitespace
                line = line.Trim();

                // if the line is empty, skip to the next line
                if (line.Length == 0)
                    continue;

                // if its the beginning of a multi-line comment remember the name of the comment block
                if (Regex.IsMatch(line, @"^\s*\#\s*<<\s*\w+"))
                {
                    comments.Add(Regex.Match(line, @"^\s*\#\s*<<\s*(\w+)").Groups[1].Value);
                }

                // if we reach the end of a comment block pop it and continue
                // if we're in a comment block but haven't reached the end, continue also
                if (comments.Count > 0)
                {
                    bool found = false;
                    foreach (string comment in comments)
                    {
                        if (Regex.IsMatch(line, @"^\s*\#\s*>>\s*" + comment))
                        {
                            found = true;
                            comments.Remove(comment);
                            break;
                        }
                    }

                    if (found == false && Regex.IsMatch(line, @"^\s*\#\s*>>"))
                    {
                        throw new Exception("Closing unopened comment block: "
                            + Regex.Match(line, @"^\s*\#\s*>>\s*(\w+)").Groups[1].Value);
                    }

                    continue;
                }

                // ignore single line comments too
                if (line.StartsWith("#"))
                    continue;

                // remove spaces around parenthesis
                line = Regex.Replace(line, @"\s*\(\s*", " (");
                line = Regex.Replace(line, @"\s*\)\s*", ")");

                // remove any comments after line
                line = Regex.Replace(line, @"#.*", "").Trim();

                // replace boolean not inside parens to cheat
                line = Regex.Replace(line, @"\(\s*!\s*", "(false == ");

                // regex replace any whitespace with a single space
                line = Regex.Replace(line, @"\s+", " ");

                // add the line into output file
                acc.Add(ReplaceStrings(line.Trim(), strings));
            }
            
            Debug("\n-------------------------\n"
                                    + "Normalizing\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));

            if (comments.Count > 0)
                throw new Exception("Unclosed comment blocks: " + String.Join(", ", comments.ToArray()));

            return acc.ToArray();
        }

        /// <summary>
        /// Make sure syntax is valid
        /// </summary>
        /// <param name="input">code to validate</param>
        public void ValidateSyntax(string[] input)
        {
            // if its valid code the line should match one of these patterns
            string[] okPatterns =
            {
                @"^requiring \S+$",                     // requiring things is okay
                @"^: \S+$",                             // labels are okay
                @"^def \S+ \(\)$",                      // no-argument function defs are okay
                @"^def \S+ \(\S+(, \S+)*\)$",           // multi-argument function defs are okay
                @"^return$",                            // return is okay
                @"^return \S+(, \S+)*$",                // returning values is okay
                @"^\S+ \(\)$",                          // no-argument function calls are okay
                @"^\S+ \(\S+(, \S+)*\)$",               // multi-argument function calls are okay
                @"^fork \S+ \(\)$",                     // no-argument function forks are okay
                @"^fork \S+ \(\S+(, \S+)*\)$",          // multi-argument function forks are okay
                @"^(put|into) \S+(, \S+)*$",            // putting into is okay
                @"^jump \S+$",                          // jumping is okay
                @"^jumpunless \S+ \S+$",                // jumping unless is okay
                @"^global( persistent)? \S+$",          // globals and persistants are okay
                @"^for \S+ from \S+ to \S+( by \S+)?",  // for is okay
                @"^next$",                              // next is okay
                @"^while \(\S+( \S+ \S+)?\)$",          // while is okay
                @"^loop$",                              // loop is okay
                @"^(else )?if \(\S+( \S+ \S+)?\)$",     // if is okay
                @"^else$",                              // else is okay
                @"^endif$",                             // endif is okay
                @"^\S+ = \S+$",                         // equals is okay
                @"^\S+ = \S+ \(\)$",                    // equalsing a no-argument function call is okay
                @"^\S+ = \S+ \(\S+(, \S+)*\)$",         // equalsing a multi-argument function call is okay
                @"^\S+ \S+= \(\)$",                     // no-argument function equals are okay
                @"^\S+ \S+= \(\S+(, \S+)*\)$"           // multi-argument function equals are okay
            };

            foreach (string checkline in input)
            {
                string[] strings;
                string line = RemoveStrings(checkline, out strings);
                bool okay = false;

                foreach (string pattern in okPatterns)
                {
                    if (Regex.IsMatch(line, pattern))
                        okay = true;
                }

                if (okay == false)
                    throw new Exception("Syntax error on or around line: " + ReplaceStrings(line, strings));
            }
        }

        /// <summary>
        /// Converts all 'a function= (b)' to 'function (a, b) into a'
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>processed script</returns>
        public string[] ProcessFunctionEquals(string[] input)
        {
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                string line = input[i];

                string[] strings;
                line = RemoveStrings(line, out strings);

                // replace x function= (y) with x = function (x, y)
                line = Regex.Replace(line, @"^(\w+)\s+(\S+)=\s+\((.+)\)$", "$1 = $2 ($1, $3)");

                // replace x function= () with x = function (x)
                line = Regex.Replace(line, @"^(\w+)\s+(\S+)=\s+\(()\)$", "$1 = $2 ($1)");

                acc.Add(ReplaceStrings(line, strings));
            }

            Debug("\n-------------------------\n"
                        + "Processing function=\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));
            return acc.ToArray();
        }

        /// <summary>
        /// Converts all 'a = b' to 'put b into a'
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>processed script</returns>
        public string[] ProcessEquals(string[] input)
        {
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                string[] strings;
                input[i] = RemoveStrings(input[i], out strings);

                string[] lines = Regex.Replace(input[i], @"(.+)\s+=\s+(.+)", "$2\ninto $1").Split('\n');

                if (lines.Count() > 1)
                {
                    if (!lines[0].Contains("("))
                    {
                        lines[0] = "put " + lines[0];
                    }

                    acc.Add(ReplaceStrings(lines[0], strings));
                    acc.Add(ReplaceStrings(lines[1], strings));
                }
                else
                {
                    acc.Add(ReplaceStrings(input[i], strings));
                }
            }

            Debug("\n-------------------------\n"
             + "Processing equals\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));
            return acc.ToArray();
        }

        /// <summary>
        /// Converts all 'if/else if/else' to nested 'if/else'
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>processed script</returns>
        public string[] ProcessElse(string[] input)
        {
            Stack<int> nesting = new Stack<int>();
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                if (input[i].StartsWith("if"))
                {
                    nesting.Push(1);
                    acc.Add(input[i]);
                }
                else if (input[i].StartsWith("else if"))
                {
                    int temp = nesting.Pop();
                    nesting.Push(temp + 1);

                    acc.Add("else");
                    acc.Add(input[i].Substring(5));
                }
                else if (input[i].StartsWith("endif"))
                {
                    for (int j = 0; j < nesting.Peek(); j++)
                    {
                        acc.Add("endif");
                    }
                    nesting.Pop();
                }
                else
                {
                    acc.Add(input[i]);
                }
            }

            Debug("\n-------------------------\n"
                         + "Processing else\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));
            return acc.ToArray();
        }

        /// <summary>
        /// Converts all if's to conditional jumps
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>processed scripts</returns>
        public string[] ProcessIf(string[] input)
        {
            Stack<int> nesting = new Stack<int>();
            Stack<Boolean> foundElse = new Stack<Boolean>();
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                string[] strings;
                input[i] = RemoveStrings(input[i], out strings);

                if (input[i].StartsWith("if"))
                {
                    // grab the conditional out
                    string condition = input[i];
                    condition = condition.Substring(condition.IndexOf('(') + 1);
                    condition = condition.Remove(condition.IndexOf(')'),1);

                    // generate a new symbol for this nest and assume there's no else
                    nesting.Push(mGenSym);
                    foundElse.Push(false);
                    mGenSym++;

                    // if it's a complex conditional, break it into a multi-line statement
                    if (condition.Contains('=') || condition.Contains('<') || condition.Contains('>'))
                    {
                        string[] cond = condition.Split(' ');
                        acc.Add(ReplaceStrings(cond[1] + " (" + cond[0] + ", " + cond[2] + ")", strings));
                        acc.Add(ReplaceStrings("into cond_" + nesting.Peek(), strings));
                        acc.Add(ReplaceStrings("jumpunless cond_" + nesting.Peek() + " end_if_" +
                            nesting.Peek(), strings));
                    }
                    else
                    {
                        acc.Add(ReplaceStrings("jumpunless " + condition + " end_if_" + nesting.Peek(), strings));
                    }
                }
                else if (input[i].StartsWith("else"))
                {
                    // keep track of whether we've found an else or not
                    foundElse.Pop();
                    foundElse.Push(true);

                    // skip the else if the if was true, otherwise execute the else
                    acc.Add("jump end_else_" + nesting.Peek());
                    acc.Add(": end_if_" + nesting.Peek());
                }
                else if (input[i].StartsWith("endif"))
                {
                    // closes on something different depending on whether there was an else
                    if (foundElse.Pop() == false)
                    {
                        acc.Add(ReplaceStrings(": end_if_" + nesting.Pop(), strings));
                    }
                    else
                    {
                        acc.Add(ReplaceStrings(": end_else_" + nesting.Pop(), strings));
                    }
                }
                else
                {
                    acc.Add(ReplaceStrings(input[i], strings));
                }
            }

            Debug("\n-------------------------\n"
                            + "Processing if\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));
            return acc.ToArray();
        }

        /// <summary>
        /// Converts all for to while/loop with iterators
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>processed script</returns>
        public string[] ProcessFor(string[] input)
        {
            Stack<string> nesting = new Stack<string>();
            Stack<string> increment = new Stack<string>();
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                string[] strings;
                input[i] = RemoveStrings(input[i], out strings);

                if (input[i].StartsWith("for "))
                {
                    // grab the arguments out
                    string[] args = input[i].Split(' ');

                    // see if they specified a step size and remember it
                    if (args.Length > 5)
                    {
                        increment.Push(args[7]);
                    }
                    else
                    {
                        increment.Push("1");
                    }

                    // track which for you're on by the variable being iterated
                    // then convert to a while loop
                    nesting.Push(args[1]);
                    acc.Add(ReplaceStrings("put " + args[3], strings));
                    acc.Add(ReplaceStrings("into " + args[1], strings));
                    if (increment.Peek().StartsWith("-"))
                    {
                        acc.Add(ReplaceStrings("while (" + args[1] + " >= " + args[5] + ")", strings));
                    }
                    else
                    {
                        acc.Add(ReplaceStrings("while (" + args[1] + " <= " + args[5] + ")", strings));
                    }
                }
                else if (input[i].StartsWith("next"))
                {
                    // pop the top variable off and close the loop
                    acc.Add(ReplaceStrings("+ (" + nesting.Peek() + ", " + increment.Pop() + ")", strings));
                    acc.Add(ReplaceStrings("into " + nesting.Peek(), strings));
                    acc.Add(ReplaceStrings("loop", strings));
                    nesting.Pop();
                }
                else
                {
                    acc.Add(ReplaceStrings(input[i], strings));
                }
            }

            Debug("\n-------------------------\n"
                            + "Processing for\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));
            return acc.ToArray();
        }

        /// <summary>
        /// Converts all whiles/loop to conditional jumps
        /// </summary>
        /// <param name="input">script</param>
        /// <returns>processed script</returns>
        public string[] ProcessWhile(string[] input)
        {
            Stack<int> nesting = new Stack<int>();
            List<string> acc = new List<string>();

            for (int i = 0; i < input.Length; i++)
            {
                string[] strings;
                input[i] = RemoveStrings(input[i], out strings);

                if (input[i].StartsWith("while"))
                {
                    // grab the conditional out
                    string condition = input[i];
                    condition = condition.Substring(condition.IndexOf('(') + 1);
                    condition = condition.Remove(condition.IndexOf(')'),1);

                    // complex conditionals require multi-line evaluation
                    if (condition.Contains('=') || condition.Contains('<') || condition.Contains('>'))
                    {
                        string[] cond = condition.Split(' ');
                        acc.Add(ReplaceStrings(": while_start_" + mGenSym, strings));
                        acc.Add(ReplaceStrings(cond[1] + " (" + cond[0] + ", " + cond[2] + ")", strings));
                        acc.Add(ReplaceStrings("into cond_" + (mGenSym + 1), strings));
                        acc.Add(ReplaceStrings("jumpunless cond_" + (mGenSym + 1) + " while_end_" + mGenSym, strings));
                        nesting.Push(mGenSym);
                        mGenSym += 2;
                    }
                    else
                    {
                        // a while is just a label followed by a conditional branch
                        acc.Add(ReplaceStrings(": while_start_" + mGenSym, strings));
                        acc.Add(ReplaceStrings("jumpunless " + condition + " while_end_" + mGenSym, strings));
                        nesting.Push(mGenSym);
                        mGenSym++;
                    }
                }
                else if (input[i].StartsWith("loop"))
                {
                    // jump back to the beginning
                    acc.Add(ReplaceStrings("jump while_start_" + nesting.Peek(), strings));
                    acc.Add(ReplaceStrings(": while_end_" + nesting.Peek(), strings));
                    nesting.Pop();
                }
                else
                {
                    acc.Add(ReplaceStrings(input[i], strings));
                }
            }

            Debug("\n-------------------------\n"
                            + "Processing while\n-------------------------\n");
            Debug(string.Join("\n", acc.ToArray()));
            return acc.ToArray();
        }

        /// <summary>
        /// Removes everything between quotes from a string replacing it with $$1$$ etc
        /// </summary>
        /// <param name="strings">place to put strings once they're removed</param>
        /// <returns>line with strings removed</returns>
        public static string RemoveStrings(string line, out string[] strings)
        {
            // if for some reason we're called on an empty line, just return an empty line
            if (line.Length == 0)
            {
                strings = new string[0];
                return "";
            }

            // its easier to store the strings in a List then convert later.
            List<string> strs = new List<string>();
            string accString = "";
            string accCode = "";
            bool inString = false;

            // special case for starting on a quotation mark
            if (line[0] == '"')
            {
                inString = true;
            }
            else
            {
                accCode = "" + line[0];
            }

            bool escaped = false;

            // check every character for " and accumulate string contents and code in separate places
            for (int i = 1; i < line.Length; i++)
            {
                if (line[i] == '\\')
                    escaped = !escaped;

                // every time we encounter a " flush one of our buffers somewhere
                if (line[i] == '"' && !escaped)
                {
                    if (inString)
                    {
                        accCode += "$$" + strs.Count + "$$";
                        strs.Add(accString);
                        accString = "";
                    }
                    inString = !inString;
                }
                else
                {
                    if (inString)
                    {
                        accString += line[i];
                    }
                    else
                    {
                        accCode += line[i];
                    }
                }

                if (line[i] != '\\')
                    escaped = false;
            }

            if (inString)
                throw new Exception("Unterminated string on line: " + line);

            strings = strs.ToArray();

            return accCode;
        }

        /// <summary>
        /// Put a removed set of strings back into a line.
        /// </summary>
        /// <param name="line">Line containing $$1$$ style placeholders</param>
        /// <param name="strings">Array of strings to put back in</param>
        /// <returns>rebuilt line</returns>
        public static string ReplaceStrings(string line, string[] strings)
        {
            for (int i = 0; i < strings.Length; i++)
            {
                line = Regex.Replace(line, @"\$\$" + i + @"\$\$", "\"" + strings[i] + "\"");
            }

            return line;
        }
    }
}
